查看原文
其他

Julia 笔记之函数

JunJunLab 老俊俊的生信笔记 2022-08-17

没关注?伸出手指点这里---

1引言

今天介绍一下 julia 中的函数相关知识。

本文内容摘抄自 Julia 中文文档

2函数

在 Julia 中定义函数的基本语法是:

julia> function f(x,y)
           x + y
       end
f (generic function with 1 method)

这个函数接收两个参数 x 和 y 并返回最后一个表达式的值,这里是 x + y。

在 Julia 中定义函数还有第二种更简洁的语法。上述的传统函数声明语法等效于以下紧凑性的“赋值形式”:

julia> f(x,y) = x + y
f (generic function with 1 method)

使用传统的括号语法调用函数:

julia> f(2,3)
5

没有括号时,表达式 f 指的是函数对象,可以像任何值一样被传递:

julia> g = f;

julia> g(2,3)
5

和变量名一样,Unicode 字符也可以用作函数名:

julia> ∑(x,y) = x + y
∑ (generic function with 1 method)

julia> ∑(23)
5

3参数类型声明

可以通过将 ::TypeName 附加到参数名称来声明函数参数的类型,就像 Julia 中的 类型声明 一样。例如,以下函数递归计算 斐波那契数列:

fib(n::Integer) = n ≤ 2 ? one(n) : fib(n-1) + fib(n-2)

并且 ::Integer 规范意味着它只有在 n 是 抽象 Integer 类型的子类型时才可调用。

4return 关键字

函数返回的值是最后计算的表达式的值,默认情况下,它是函数定义主体中的最后一个表达式。在上一小节的示例函数 f 中,返回值是表达式的 x + y 值。与在 C 语言和大多数其他命令式或函数式语言中一样,return 关键字会让函数立即返回,从而提供返回值的表达式:

function g(x,y)
    return x * y
    x + y
end

比较:

julia> f(x,y) = x + y
f (generic function with 1 method)

julia> function g(x,y)
           return x * y
           x + y
       end
g (generic function with 1 method)

julia> f(2,3)
5

julia> g(2,3)
6

5返回类型

也可以使用 :: 运算符在函数声明中指定返回类型。这可以将返回值转换为指定的类型:

julia> function g(x, y)::Int8
           return x * y
       end;

julia> typeof(g(12))
Int8

这个函数将忽略 x 和 y 的类型,返回 Int8 类型的值。有关返回类型的更多信息。

6返回 nothing

对于不需要任何返回值的函数(只用来产生副作用的函数), Julia 中的写法为返回值 nothing:

function printx(x)
    println("x = $x")
    return nothing
end

有两种比 return nothing 更短的写法:一种是直接写 return 这会隐式的返回 nothing。另一种是在函数的最后后一行写上 nothing,因为函数会隐式的返回最后一个表达式的值。哪种写法使用哪一种取决于代码风格的偏好。

7操作符也是函数

在 Julia 中,大多数操作符只不过是支持特殊语法的函数( && 和|| 等具有特殊评估语义的操作符除外,他们不能是函数,因为短路求值要求在计算整个表达式的值之前不计算每个操作数)。因此,您也可以使用带括号的参数列表来使用它们,就和任何其他函数一样:

julia> 1 + 2 + 3
6

julia> +(1,2,3)
6

例如 + 和 * ,进行赋值和传参,就像其它函数传参一样,然而,函数以 f 命名时不再支持中缀表达式:

julia> f = +;

julia> f(1,2,3)
6

8具有特殊名称的操作符

有一些特殊的表达式对应的函数调用没有显示的函数名称,它们是:

9匿名函数

函数在 Julia 里是一等公民:可以指定给变量,并使用标准函数调用语法通过被指定的变量调用。函数可以用作参数,也可以当作返回值。函数也可以不带函数名称地匿名创建,使用语法如下:

julia> x -> x^2 + 2x - 1
#1 (generic function with 1 method)

julia> function (x)
           x^2 + 2x - 1
       end
#3 (generic function with 1 method)

这样就创建了一个接受一个参数 x 并返回当前值的多项式 x^2+2x-1 的函数。注意结果是个 泛型函数,但是带了编译器生成的连续编号的名字。

匿名函数最主要的用法是传递给接收函数作为参数的函数。一个经典的例子是 map ,为数组的每个元素应用一次函数,然后返回一个包含结果值的新数组:

julia> map(round, [1.23.51.7])
3-element Vector{Float64}:
 1.0
 4.0
 2.0

如果做为第一个参数传递给 map 的转换函数已经存在,那直接使用函数名称是没问题的。但是通常要使用的函数还没有定义好,这样使用匿名函数就更加方便

julia> map(x -> x^2 + 2x - 1, [13, -1])
3-element Vector{Int64}:
  2
 14
 -2

接受多个参数的匿名函数写法可以使用语法 (x,y,z)->2x+y-z而无参匿名函数写作 ()->3 。无参函数的这种写法看起来可能有些奇怪,不过它对于延迟计算很有必要。这种用法会把代码块包进一个无参函数中,后续把它当做 f 调用。

10解构赋值和多返回值

逗号分隔的变量列表(可选地用括号括起来)可以出现在赋值的左侧:右侧的值通过迭代并依次分配给每个变量来解构:

julia> (a,b,c) = 1:3
1:3

julia> b
2

右边的值应该是一个至少与左边的变量数量一样长的迭代器,可用于通过返回元组或其他可迭代值从函数返回多个值。例如,以下函数返回两个值:

julia> function foo(a,b)
           a+b, a*b
       end
foo (generic function with 1 method)

julia> foo(2,3)
(56)

解构赋值将每个值提取到一个变量中:

julia> x, y = foo(2,3)
(56)

julia> x
5

julia> y
6

另一个常见用途是交换变量:

julia> y, x = x, y
(56)

julia> x
6

julia> y
5

如果只需要迭代器元素的一个子集,一个常见的惯例是将忽略的元素分配给一个只包含下划线 _ 的变量,这是一个无效的变量名:

julia> _, _, _, d = 1:10
1:10

julia> d
4

其他有效的左侧表达式可以用作赋值列表的元素,它们将调用 setindex!setproperty!,或者递归地解构迭代器的各个元素:

julia> X = zeros(3);

julia> X[1], (a,b) = (1, (23))
(1, (23))

julia> X
3-element Vector{Float64}:
 1.0
 0.0
 0.0

julia> a
2

julia> b
3

如果赋值列表中的最后一个符号后缀为 ...(称为 slurping),那么它将被分配给右侧迭代器剩余元素的集合或其惰性迭代器

julia> a, b... = "hello"
"hello"

julia> a
'h': ASCII/Unicode U+0068 (category Ll: Letter, lowercase)

julia> b
"ello"

julia> a, b... = Iterators.map(abs2, 1:4)
Base.Generator{UnitRange{Int64}, typeof(abs2)}(abs2, 1:4)

julia> a
1

julia> b
Base.Iterators.Rest{Base.Generator{UnitRange{Int64}, typeof(abs2)}, Int64}(Base.Generator{UnitRange{Int64}, typeof(abs2)}(abs2, 1:4), 1)

11变参函数

定义有任意个参数 的函数会带来很多便利。这类函数通常被称为“变参”函数,即“参数数量可变”的简称。你可以通过在 最后一个参数后增加省略号 来定义一个变参函数:

julia> bar(a,b,x...) = (a,b,x)
bar (generic function with 1 method)

变量 a 和 b 和以前一样被绑定给前两个参数,后面的参数整个做为迭代集合被绑定到变量 x 上 :

julia> bar(1,2)
(12, ())

julia> bar(1,2,3)
(12, (3,))

julia> bar(1234)
(12, (34))

julia> bar(1,2,3,4,5,6)
(12, (3456))

在所有这些情况下,x 被绑定到传递给 bar 的尾随值的元组

另一方面,将可迭代集中包含的值拆解为单独的参数进行函数调用通常很方便。要实现这一点,需要在函数调用中额外使用 ... 而不仅仅只是变量:

julia> x = (34)
(34)

julia> bar(1,2,x...)
(12, (34))

在这个情况下一组值会被精确切片成一个可变参数调用,这里参数的数量是可变的。但是并不需要成为这种情况:

julia> x = (234)
(234)

julia> bar(1,x...)
(12, (34))

julia> x = (1234)
(1234)

julia> bar(x...)
(12, (34))

进一步,拆解给函数调用中的可迭代对象不需要是个元组:

julia> x = [3,4]
2-element Vector{Int64}:
 3
 4

julia> bar(1,2,x...)
(12, (34))

julia> x = [1,2,3,4]
4-element Vector{Int64}:
 1
 2
 3
 4

julia> bar(x...)
(12, (34))

12可选参数

在很多情况下,函数参数有合理的默认值,因此也许不需要显式地传递。例如,Dates 模块中的 Date(y, [m, d]) 函数对于给定的年(year)y、月(mouth)m、日(data)d 构造了 Date 类型。但是,m 和 d 参数都是可选的,默认值都是 1。这行为可以简述为:

function Date(y::Int64, m::Int64=1, d::Int64=1)
    err = validargs(Date, y, m, d)
    err === nothing || throw(err)
    return Date(UTD(totaldays(y, m, d)))
end

通过此定义,函数调用时可以带有一个、两个或三个参数,并且在只有一个或两个参数被指定时后,自动传递 1 为未指定参数值:

julia> using Dates

julia> Date(20001212)
2000-12-12

julia> Date(200012)
2000-12-01

julia> Date(2000)
2000-01-01

13关键字参数

某些函数需要大量参数,或者具有大量行为。记住如何调用这样的函数可能很困难。关键字参数允许通过名称而不是仅通过位置来识别参数

例如,考虑绘制一条线的函数 plot。这个函数可能有很多选项,用来控制线条的样式、宽度、颜色等。如果它接受关键字参数,一个可行的调用可能看起来像 plot(x, y, width=2),这里我们仅指定线的宽度。请注意,这样做有两个目的。调用更可读,因为我们能以其意义标记参数。也使得大量参数的任意子集都能以任意次序传递。

具有关键字参数的函数在签名中使用分号定义

function plot(x, y; style="solid", width=1, color="black")
    ###
end

在函数调用时,分号是可选的:可以调用 plot(x, y, width=2) 或 plot(x, y; width=2),但前者的风格更为常见。显式的分号只有在传递可变参数或下文中描述的需计算的关键字时是必要的。

关键字参数的默认值只在必需时求值(当相应的关键字参数没有被传入),并且按从左到右的顺序求值,因为默认值的表达式可能会参照先前的关键字参数。

关键字参数的类型可以通过如下的方式显式指定

function f(;x::Int=1)
    ###
end

关键字参数也可以在变参函数中使用:

function plot(x...; style="solid")
    ###
end

附加的关键字参数可用 ... 收集,正如在变参函数中:

function f(x; y=0, kwargs...)
    ###
end

在 f 中,kwargs 将是一个在命名元组上的不可变键值迭代器。具名元组(以及带有 Symbol 键的字典)可以在调用中使用分号作为关键字参数传递,例如 f(x, z=1; kwargs...)。

如果一个关键字参数在方法定义中未指定默认值那么它就是必需的:如果调用者没有为其赋值,那么将会抛出一个 UndefKeywordError 异常:

function f(x; y)
    ###
end
f(3, y=5# ok, y is assigned
f(3)      # throws UndefKeywordError(:y)

在分号后也可传递 key => value 表达式。例如,plot(x, y; :width => 2) 等价于 plot(x, y, width=2)。当关键字名称需要在运行时被计算时,这就很实用了。

当分号后出现裸标识符或点表达式时,标识符或字段名称隐含关键字参数名称。例如plot(x, y; width) 等价于plot(x, y; width=width)plot(x, y; options.width) 等价于plot(x, y; width=options.width)

可选参数的性质使得可以多次指定同一参数的值。例如,在调用 plot(x, y; options..., width=2) 的过程中,options 结构也能包含一个 width 的值。在这种情况下,最右边的值优先级最高;在此例中,width 的值可以确定是 2。但是,显式地多次指定同一参数的值是不允许的,例如 plot(x, y, width=2, width=3),这会导致语法错误。

14函数参数中的 Do 结构

把函数作为参数传递给其他函数是一种强大的技术,但它的语法并不总是很方便。当函数参数占据多行时,这样的调用便特别难以编写。例如,考虑在具有多种情况的函数上调用 map:

map(x->begin
           if x < 0 && iseven(x)
               return 0
           elseif x == 0
               return 1
           else
               return x
           end
       end,
    [A, B, C])

Julia 提供了一个保留字 do,用于更清楚地重写此代码:

map([A, B, C]) do x
    if x < 0 && iseven(x)
        return 0
    elseif x == 0
        return 1
    else
        return x
    end
end

do x 语法创建一个带有参数 x 的匿名函数,并将其作为第一个参数传递给 map。类似地,do a,b 将创建一个有两个参数的匿名函数。请注意,do (a,b) 将创建一个单参数匿名函数,其参数是一个要解构的元组。一个简单的 do 会声明接下来是一个形式为 () -> ... 的匿名函数。

这些参数如何初始化取决于「外部」函数;在这里,map 将会依次将 x 设置为 A、B、C,再分别调用调用匿名函数,正如在 map(func, [A, B, C]) 语法中所发生的。

这种语法使得更容易使用函数来有效地扩展语言,因为调用看起来就像普通代码块。有许多可能的用法与 map 完全不同,比如管理系统状态。例如,有一个版本的 open 可以通过运行代码来确保已经打开的文件最终会被关闭

open("outfile""w"do io
    write(io, data)
end

这是通过以下定义实现的:

function open(f::Function, args...)
    io = open(args...)
    try
        f(io)
    finally
        close(io)
    end
end

15函数的复合与链式调用

Julia 中的多个函数可以用函数复合或管道连接(链式调用)组合起来。

函数的复合指的是把多个函数绑定到一起,然后作用于最先调用那个函数的参数。你可以使用函数复合运算符 (∘) 来组合函数,这样一来 (f ∘ g)(args...) 就等价于 f(g(args...)).

你可以在 REPL 和合理配置的编辑器中用 \circ<tab> 输入函数复合运算符。

例如, sqrt 和 + 可以用下面这种方式组合:

julia> (sqrt ∘ +)(36)
3.0

这个语句先把数字相加,再对结果求平方根。

下一个例子组合了三个函数并把新函数作用到一个字符串组成的数组上:

julia> map(first ∘ reverse ∘ uppercase, split("you can compose functions like this"))
6-element Vector{Char}:
 'U': ASCII/Unicode U+0055 (category Lu: Letter, uppercase)
 'N': ASCII/Unicode U+004E (category Lu: Letter, uppercase)
 'E': ASCII/Unicode U+0045 (category Lu: Letter, uppercase)
 'S': ASCII/Unicode U+0053 (category Lu: Letter, uppercase)
 'E': ASCII/Unicode U+0045 (category Lu: Letter, uppercase)
 'S': ASCII/Unicode U+0053 (category Lu: Letter, uppercase)

函数的链式调用(有时也称 “使用管道” 把数据送到一系列函数中去)指的是把一个函数作用到前一个函数的输出上:

julia> 1:10 |> sum |> sqrt
7.416198487095663

在这里, sum 函数求出的和被传递到 sqrt 函数作为参数。等价的函数复合写法是:

julia> (sqrt ∘ sum)(1:10)
7.416198487095663

管道运算符还可以和广播一起使用(.|>),这提供了一个有用的链式调用/管道+向量化运算的组合语法(接下来将描述):

julia> ["a""list""of""strings"] .|> [uppercase, reverse, titlecase, length]
4-element Vector{Any}:
  "A"
  "tsil"
  "Of"
 7

16向量化函数的点语法

任何 Julia 函数 f 能够以元素方式作用于任何数组(或者其它集合),这通过语法 f.(A) 实现。例如,sin 可以作用于向量 A 中的所有元素,如下所示:

julia> A = [1.02.03.0]
3-element Vector{Float64}:
 1.0
 2.0
 3.0

julia> sin.(A)
3-element Vector{Float64}:
 0.8414709848078965
 0.9092974268256817
 0.1411200080598672

当然,你如果为 f 编写了一个专门的「向量化」方法,例如通过 f(A::AbstractArray) = map(f, A),可以省略点号,这和 f.(A) 一样高效。但这种方法要求你事先决定要进行向量化的函数。

更一般地,f.(args...) 实际上等价于 broadcast(f, args...),它允许你操作多个数组(甚至是不同形状的),或是数组和标量的混合(请参阅 Broadcasting)。例如,如果有 f(x,y) = 3x + 4y,那么 f.(pi,A) 将为 A 中的每个 a 返回一个由 f(pi,a) 组成的新数组,而 f.(vector1,vector2) 将为每个索引 i 返回一个由 f(vector1[i],vector2[i]) 组成的新向量(如果向量具有不同的长度则会抛出异常):

julia> f(x,y) = 3x + 4y;

julia> A = [1.02.03.0];

julia> B = [4.05.06.0];

julia> f.(pi, A)
3-element Vector{Float64}:
 13.42477796076938
 17.42477796076938
 21.42477796076938

julia> f.(A, B)
3-element Vector{Float64}:
 19.0
 26.0
 33.0

此外,嵌套的 f.(args...) 调用会被融合到一个 broadcast 循环中。例如,sin.(cos.(X)) 等价于 broadcast(x -> sin(cos(x)), X),类似于 [sin(cos(x)) for x in X]:在 X 上只有一个循环,并且只为结果分配了一个数组。相反,在典型的「向量化」语言中,sin(cos(X)) 首先会为 tmp=cos(X) 分配第一个临时数组,然后在单独的循环中计算 sin(tmp),再分配第二个数组。 这种循环融合不是可能发生也可能不发生的编译器优化,只要遇到了嵌套的 f.(args...) 调用,它就是一个语法保证。技术上,一旦遇到「非点」函数调用,融合就会停止;例如,在 sin.(sort(cos.(X))) 中,由于插入的 sort 函数,sin 和 cos 无法被合并。

最后,最大效率通常在向量化操作的输出数组被预分配时实现这样重复调用就不会一次又一次地为结果分配新数组(请参阅 输出预分配)。一个方便的语法是 X .= ...,它等价于 broadcast!(identity, X, ...),除了上面提到的,broadcast! 循环可与任何嵌套的「点」调用融合。例如,X .= sin.(Y) 等价于 broadcast!(sin, X, Y),用 sin.(Y) in-place 覆盖 X。如果左边是数组索引表达式,例如 X[2:end] .= sin.(Y),那就将 broadcast! 转换在一个 view 上,例如 broadcast!(sin, view(X, 2:lastindex(X)), Y),这样左侧就被 in-place 更新了。

由于在表达式中为许多操作和函数调用添加点可能很乏味并导致难以阅读的代码,宏 @. 用于将表达式中的每个函数调用、操作和赋值转换为「点」版本:

julia> Y = [1.02.03.04.0];

julia> X = similar(Y); # pre-allocate output array

julia> @. X = sin(cos(Y)) # equivalent to X .= sin.(cos.(Y))
4-element Vector{Float64}:
  0.5143952585235492
 -0.4042391538522658
 -0.8360218615377305
 -0.6080830096407656

像 .+ 这样的二元(或一元)运算符使用相同的机制进行管理:它们等价于 broadcast 调用且可与其它嵌套的「点」调用融合。X .+= Y 等等价于 X .= X .+ Y,结果为一个融合的 in-place 赋值;另见 dot operators。

您也可以使用 |> 将点操作与函数链组合在一起,如本例所示:

julia> [1:5;] .|> [x->x^2, inv, x->2*x, -, isodd]
5-element Vector{Real}:
    1
    0.5
    6
   -4
 true

17结尾

这部分内容有些比较难懂,需要时间好好消化。


您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存